package net.stogi.unicore.beyond.back.model.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;

import net.stogi.unicore.beyond.api.model.InputDefinition;
import net.stogi.unicore.beyond.api.model.InputGroup;
import net.stogi.unicore.beyond.api.model.InputType;
import net.stogi.unicore.beyond.api.model.OutputDefinition;
import net.stogi.unicore.beyond.api.model.OutputGroup;
import net.stogi.unicore.beyond.api.model.OutputType;
import net.stogi.unicore.beyond.api.model.PropertyType;
import net.stogi.unicore.beyond.api.model.PropertyValue;
import net.stogi.unicore.beyond.api.model.TaskDefinition;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;

public class ModelUtils {

	private static final Logger log = LoggerFactory.getLogger(ModelUtils.class);
	
	public static final InputDefinition copyInputDefinition(InputDefinition source) {
		return copyInputDefinition(source, true);
	}

	public static final InputDefinition copyInputDefinition(InputDefinition source, boolean deep) {
		InputDefinition target = new InputDefinition();

		BeanUtils.copyProperties(source, target, new String[] { "inputGroup", "inputType", "properties" });
		target.setInputType(copyInputType(source.getInputType(), deep));
		if (deep) {
			for (Entry<String, PropertyValue> entry : source.getProperties().entrySet()) {
				target.getProperties().put(entry.getKey(), copyPropertyValue(entry.getValue()));
			}
		}

		return target;
	}

	public static final InputGroup copyInputGroup(InputGroup source) {
		return copyInputGroup(source, true);
	}

	public static final InputGroup copyInputGroup(InputGroup source, boolean deep) {
		InputGroup target = new InputGroup();

		BeanUtils.copyProperties(source, target, new String[] { "inputDefinitions", "taskDefinition" });
		if (deep) {
			for (InputDefinition inputDefinition : source.getInputDefinitions()) {
				InputDefinition copyOfInputDefinition = copyInputDefinition(inputDefinition);
				copyOfInputDefinition.setInputGroup(target);
				target.getInputDefinitions().add(copyOfInputDefinition);
			}
		}

		return target;
	}

	public static final InputType copyInputType(InputType source) {
		return copyInputType(source, true);
	}

	public static final InputType copyInputType(InputType source, boolean deep) {
		InputType target = new InputType();

		BeanUtils.copyProperties(source, target, new String[] { "properties" });
		if (deep) {
			for (Entry<String, PropertyType> entry : source.getProperties().entrySet()) {
				target.getProperties().put(entry.getKey(), copyPropertyType(entry.getValue()));
			}
		}

		return target;
	}

	public static final OutputDefinition copyOutputDefinition(OutputDefinition source) {
		return copyOutputDefinition(source, true);
	}

	public static final OutputDefinition copyOutputDefinition(OutputDefinition source, boolean deep) {
		OutputDefinition target = new OutputDefinition();

		BeanUtils.copyProperties(source, target, new String[] { "outputGroup", "outputType", "properties" });
		target.setOutputType(copyOutputType(source.getOutputType(), deep));
		if (deep) {
			for (Entry<String, PropertyValue> entry : source.getProperties().entrySet()) {
				target.getProperties().put(entry.getKey(), copyPropertyValue(entry.getValue()));
			}
		}

		return target;
	}

	public static final OutputGroup copyOutputGroup(OutputGroup source) {
		return copyOutputGroup(source, true);
	}

	public static final OutputGroup copyOutputGroup(OutputGroup source, boolean deep) {
		OutputGroup target = new OutputGroup();

		BeanUtils.copyProperties(source, target, new String[] { "outputDefinitions", "taskDefinition" });
		if (deep) {
			for (OutputDefinition outputDefinition : source.getOutputDefinitions()) {
				OutputDefinition copyOfOutputDefinition = copyOutputDefinition(outputDefinition);
				copyOfOutputDefinition.setOutputGroup(target);
				target.getOutputDefinitions().add(copyOfOutputDefinition);
			}
		}

		return target;
	}

	public static final OutputType copyOutputType(OutputType source) {
		return copyOutputType(source, true);
	}

	public static final OutputType copyOutputType(OutputType source, boolean deep) {
		OutputType target = new OutputType();

		BeanUtils.copyProperties(source, target, new String[] { "properties" });
		if (deep) {
			for (Entry<String, PropertyType> entry : source.getProperties().entrySet()) {
				target.getProperties().put(entry.getKey(), copyPropertyType(entry.getValue()));
			}
		}

		return target;
	}

	public static final PropertyType copyPropertyType(PropertyType source) {
		PropertyType target = new PropertyType();

		BeanUtils.copyProperties(source, target);

		return target;
	}

	public static final PropertyValue copyPropertyValue(PropertyValue source) {
		PropertyValue target = new PropertyValue();

		BeanUtils.copyProperties(source, target, new String[] { "propertyType" });
		target.setPropertyType(copyPropertyType(source.getPropertyType()));

		return target;
	}

	public static final TaskDefinition copyTaskDefinition(TaskDefinition source) {
		return copyTaskDefinition(source, true);
	}

	public static final TaskDefinition copyTaskDefinition(TaskDefinition source, boolean deep) {
		TaskDefinition target = new TaskDefinition();

		BeanUtils.copyProperties(source, target, new String[] { "inputGroups", "outputGroups" });
		if (deep) {
			for (InputGroup inputGroup : source.getInputGroups()) {
				InputGroup copyInputGroup = copyInputGroup(inputGroup);
				copyInputGroup.setTaskDefinition(target);
				target.getInputGroups().add(copyInputGroup);
			}
			for (OutputGroup outputGroup : source.getOutputGroups()) {
				OutputGroup copyOutputGroup = copyOutputGroup(outputGroup);
				copyOutputGroup.setTaskDefinition(target);
				target.getOutputGroups().add(copyOutputGroup);
			}
		}

		return target;
	}
	
	public static final void updateInputDefinition(InputDefinition source, InputDefinition target) {
		BeanUtils.copyProperties(source, target, new String[] { "inputGroup", "inputType", "properties" });

		// Update / add / remove properties.
		{
			// Remove input definitions from target that doesn't exists in source.
			List<PropertyValue> propertiesForRemoval = new ArrayList<PropertyValue>();

			for (PropertyValue targetProperty : target.getProperties().values()) {
				if (targetProperty.getId() != null && source.selectPropertyWithId(targetProperty.getId()) == null) {
					propertiesForRemoval.add(targetProperty);

					log.trace("Input definition with given id was marked for removal: [ inputDefinition.id = '{}' ]", target.getId());
				}
			}

			if (target.getProperties().values().removeAll(propertiesForRemoval)) {
				log.debug("Properties was removed: [ removed.size = '{}' ]", propertiesForRemoval.size());
			}

			// Add new / update properties.
			for (PropertyValue sourceProperty : source.getProperties().values()) {
				Long id = sourceProperty.getId();

				// Update property.
				if (id != null) {
					updateProperty(sourceProperty, target.selectPropertyWithId(id));

					log.debug("Property was updated: [ property.id = '{}' ]", sourceProperty.getId());
				}
				// Add property.
				else {
					target.getProperties().put(sourceProperty.getPropertyType().getKey(), sourceProperty);

					log.debug("New property was added: [ property = '{}' ]", sourceProperty);
				}
			}
		}
	}

	public static final void updateInputGroup(InputGroup source, InputGroup target) {
		BeanUtils.copyProperties(source, target, new String[] { "inputDefinitions", "taskDefinition" });

		// Update / add / remove input definitions.
		{
			// Remove input definitions from target that doesn't exists in source.
			List<InputDefinition> inputDefinitionsForRemoval = new ArrayList<InputDefinition>();

			for (InputDefinition targetInputDefinition : target.getInputDefinitions()) {
				Long id = targetInputDefinition.getId();
				if (id != null && source.selectInputDefinitionWithId(id) == null) {
					inputDefinitionsForRemoval.add(targetInputDefinition);
					targetInputDefinition.setInputGroup(null);

					log.trace("Input definition with given id was marked for removal: [ inputDefinition.id = '{}' ]", id);
				}
			}

			if (target.getInputDefinitions().removeAll(inputDefinitionsForRemoval)) {
				log.debug("Input definitions was removed: [ removed.size = '{}' ]", inputDefinitionsForRemoval.size());
			}

			List<InputDefinition> inputDefinitionsForAddition = new ArrayList<InputDefinition>();

			// Add new / update input definitions.
			for (InputDefinition sourceInputDefinition : source.getInputDefinitions()) {
				Long id = sourceInputDefinition.getId();

				// Update input definition.
				if (id != null) {
					updateInputDefinition(sourceInputDefinition, target.selectInputDefinitionWithId(id));

					log.debug("Input definition was updated: [ inputDefinition.id = '{}' ]", sourceInputDefinition.getId());
				}
				// Add input definition.
				else {
					inputDefinitionsForAddition.add(sourceInputDefinition);
					sourceInputDefinition.setInputGroup(target);

					log.debug("New input definition was added: [ inputDefinition.name = '{}' ]", sourceInputDefinition.getName());
				}
			}

			if (target.getInputDefinitions().addAll(inputDefinitionsForAddition)) {
				log.debug("Input definitions was added: [ added.size = '{}' ]", inputDefinitionsForAddition.size());
			}
		}
	}

	public static final void updateOutputDefinition(OutputDefinition source, OutputDefinition target) {
		BeanUtils.copyProperties(source, target, new String[] { "properties", "outputType" });

		// Update / add / remove properties.
		{
			// Remove output definitions from target that doesn't exists in source.
			List<PropertyValue> propertiesForRemoval = new ArrayList<PropertyValue>();

			for (PropertyValue targetProperty : target.getProperties().values()) {
				if (targetProperty.getId() != null && source.selectPropertyWithId(targetProperty.getId()) == null) {
					propertiesForRemoval.add(targetProperty);

					log.trace("Output definition with given id was marked for removal: [ outputDefinition.id = '{}' ]", target.getId());
				}
			}

			if (target.getProperties().values().removeAll(propertiesForRemoval)) {
				log.debug("Properties was removed: [ removed.size = '{}' ]", propertiesForRemoval.size());
			}

			// Add new / update properties.
			for (PropertyValue sourceProperty : source.getProperties().values()) {
				Long id = sourceProperty.getId();

				// Update property.
				if (id != null) {
					updateProperty(sourceProperty, target.selectPropertyWithId(id));

					log.debug("Property was updated: [ property.id = '{}' ]", sourceProperty.getId());
				}
				// Add property.
				else {
					target.getProperties().put(sourceProperty.getPropertyType().getKey(), sourceProperty);

					log.debug("New property was added: [ property = '{}' ]", sourceProperty);
				}
			}
		}
	}

	public static final void updateOutputGroup(OutputGroup source, OutputGroup target) {
		BeanUtils.copyProperties(source, target, new String[] { "outputDefinitions" });

		// Update / add / remove output definitions.
		{
			// Remove output definitions from target that doesn't exists in source.
			List<OutputDefinition> outputDefinitionsForRemoval = new ArrayList<OutputDefinition>();

			for (OutputDefinition targetOutputDefinition : target.getOutputDefinitions()) {
				if (targetOutputDefinition.getId() != null && source.selectOutputDefinitionWithId(targetOutputDefinition.getId()) == null) {
					outputDefinitionsForRemoval.add(targetOutputDefinition);
					targetOutputDefinition.setOutputGroup(null);

					log.trace("Output definition with given id was marked for removal: [ outputDefinition.id = '{}' ]", targetOutputDefinition
							.getId());
				}
			}

			if (target.getOutputDefinitions().removeAll(outputDefinitionsForRemoval)) {
				log.debug("Output definitions was removed: [ removed.size = '{}' ]", outputDefinitionsForRemoval.size());
			}

			// Add new / update output definitions.
			for (OutputDefinition sourceOutputDefinition : source.getOutputDefinitions()) {
				Long id = sourceOutputDefinition.getId();

				// Update output definition.
				if (id != null) {
					updateOutputDefinition(sourceOutputDefinition, target.selectOutputDefinitionWithId(id));

					log.debug("Output definition was updated: [ outputDefinition.id = '{}' ]", sourceOutputDefinition.getId());
				}
				// Add output definition.
				else {
					target.getOutputDefinitions().add(sourceOutputDefinition);
					sourceOutputDefinition.setOutputGroup(target);

					log.debug("New output definition was added: [ outputDefinition.name = '{}' ]", sourceOutputDefinition.getName());
				}
			}
		}
	}

	public static final void updateProperty(PropertyValue source, PropertyValue target) {
		BeanUtils.copyProperties(source, target, new String[] { "propertyType" });
	}

	public static final void updateTaskDefinition(TaskDefinition source, TaskDefinition target) {
		BeanUtils.copyProperties(source, target, new String[] { "inputGroups", "outputGroups" });

		// Update / add / remove input groups.
		{
			// Remove input groups from target that doesn't exists in source.
			{
				List<InputGroup> inputGroupsForRemoval = new ArrayList<InputGroup>();

				for (InputGroup targetInputGroup : target.getInputGroups()) {
					Long id = targetInputGroup.getId();

					log.trace("Input group with given name: [ inputGroup.name = '{}' ]", targetInputGroup.getName());
					
					if (source.selectInputGroupWithId(id) == null) {
						inputGroupsForRemoval.add(targetInputGroup);
						targetInputGroup.setTaskDefinition(null);

						log.trace("Input group with given id was marked for removal: [ inputGroup.id = '{}' ]", id);
					}
				}

				if (target.getInputGroups().removeAll(inputGroupsForRemoval)) {
					log.debug("Input groups was removed: [ removed.size = '{}' ]", inputGroupsForRemoval.size());
				}
			}

			// Add new / update input groups.
			{
				List<InputGroup> inputGroupsForAddition = new ArrayList<InputGroup>();

				for (InputGroup sourceInputGroup : source.getInputGroups()) {
					Long id = sourceInputGroup.getId();

					// Update input group.
					if (id != null) {
						updateInputGroup(sourceInputGroup, target.selectInputGroupWithId(id));

						log.debug("Input group was updated: [ inputGroup.id = '{}' ]", id);
					}
					// Add input group.
					else {
						inputGroupsForAddition.add(sourceInputGroup);
						sourceInputGroup.setTaskDefinition(target);

						log.debug("New input group was added: [ inputGroup.name = '{}' ]", sourceInputGroup.getName());
					}
				}

				if (target.getInputGroups().addAll(inputGroupsForAddition)) {
					log.debug("Input groups was added: [ added.size = '{}' ]", inputGroupsForAddition.size());
				}
			}
		}

		// Update / add / remove output groups.
		{
			// Remove output groups from target that doesn't exists in source.
			List<OutputGroup> outputGroupsForRemoval = new ArrayList<OutputGroup>();

			for (OutputGroup targetOutputGroup : target.getOutputGroups()) {
				Long id = targetOutputGroup.getId();
				if (source.selectOutputGroupWithId(id) == null) {
					outputGroupsForRemoval.add(targetOutputGroup);
					targetOutputGroup.setTaskDefinition(null);

					log.trace("Output group with given id was marked for removal: [ inputGroup.id = '{}' ]", id);
				}
			}

			if (target.getOutputGroups().removeAll(outputGroupsForRemoval)) {
				log.debug("Output groups was removed: [ removed.size = '{}' ]", outputGroupsForRemoval.size());
			}

			// Add new / update output groups.
			for (OutputGroup sourceOutputGroup : source.getOutputGroups()) {
				Long id = sourceOutputGroup.getId();

				// Update output group.
				if (id != null) {
					updateOutputGroup(sourceOutputGroup, target.selectOutputGroupWithId(id));

					log.debug("Output group was updated: [ outputGroup.id = '{}' ]", sourceOutputGroup.getId());
				}
				// Add output group.
				else {
					target.getOutputGroups().add(sourceOutputGroup);
					sourceOutputGroup.setTaskDefinition(target);

					log.debug("New output group was added: [ outputGroup.name = '{}' ]", sourceOutputGroup.getName());
				}
			}
		}
	}

	private ModelUtils() {}
}
